package org.hawk.gwt.ppc.scanner.utils;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import org.hawk.gwt.ppc.javacc.JavaParserTreeConstants;
import org.hawk.gwt.ppc.javacc.JavaScanner;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.UniversalSourcesLoader;

/**
 * <p>Represents simple utility to load and parse java source files.
 * Allows to get {@link JavaScope}s for different positions of scanner.</p>
 * <p>TODO's list:</p>
 * <ol>
 * <li>add support of modifiers
 * <li>add getters for inner classes, imports, superclass, 
 * implemented interfaces.
 * <li>add methods for declared entities only (excluding superclass and 
 * implemented interfaces entities)
 * <li>add support of type of entity (class/interface/enum/annotation)
 * </ol>
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class JavaClassDescriptor {
	static final String CLASS_KEYWORD = "class";
	static final String INTERFACE_KEYWORD = "interface";
	static final String PACKAGE_KEYWORD = "package";
	static final String END_LINE_KEYWORD = ";";
	static final String IMPORT_KEYWORD = "import";
	static final String ENUM_KEYWORD = "enum";
	static final String ANNOTATION_KEYWORD = "@interface";
	
	private SourcesLoader loader;
	private String className;
	private JavaScope compilationUnit;
	private JavaClassDescriptor arrayElement;
	/**
	 * Creates descriptor with default classpath source loader.
	 * @param className
	 * @throws IOException
	 * @throws ParseException
	 */
	JavaClassDescriptor(String className) 
			throws IOException, ParseException{
		this(className,new UniversalSourcesLoader());
	}
	/**
	 * Creates desciptor with specific sources loader.
	 * @param className
	 * @param loader
	 * @throws ParseException
	 * @throws IOException
	 */
	JavaClassDescriptor(String className,SourcesLoader loader) 
			throws ParseException, IOException{
		if(className==null){
			throw new NullPointerException("className cannot be NULL");
		}
		if(loader==null){
			throw new NullPointerException("loader cannot be NULL");
		}
		this.loader = loader;
		this.className = className;
		readClass(className);
	}
	private void readClass(String className)
			throws ParseException, IOException {
		if(!className.endsWith("[]")){
			readClass();
			return;
		}
		className = className.substring(0,className.length()-2);
		arrayElement = new JavaClassDescriptor(
			className,loader);
		compilationUnit = new ArraysTypeScope(arrayElement);
	}
	
	private void readClass() throws ParseException, IOException {
		CompilationUnitScanner scanner = new CompilationUnitScanner();
		InputStream stream = getClassAsStream();
		try {
			scanner.startScan(stream, null);
		}
		finally{
			stream.close();
		}
	}
	
	private InputStream getClassAsStream() throws IOException {
		URL url = loader.getClass(className);
		if(url==null){
			throw new IOException(
				"Unable to find source for class with name "+
				className+" using loader "+loader);
		}
		InputStream stream = url.openStream();
		return stream;
	}
	/**
	 * Creates class descriptor for already loaded class.
	 * All methods remain valid except for sources indices.
	 * TODO list:
	 * - disassembling 
	 * - debug lines from <code>.class</code> file.
	 * @param clazz
	 */
	JavaClassDescriptor(Class<?> clazz) {
		this.className = clazz.getName();
		this.compilationUnit = new JavaClassScope(clazz);
		if(clazz.isArray()){
			arrayElement = new JavaClassDescriptor(clazz.getComponentType());
		}
	}
	/**
	 * @return name of the top-level class this descriptor is created for.
	 */
	public String getClassName() {
		return className;
	}
	/**
	 * @return top-level {@link JavaScope} object (representing top-level 
	 * class with the name descriptor created for).
	 * @see #getClassName()
	 */
	public JavaTypeScope getCompilationUnit() {
		return (JavaTypeScope) compilationUnit;
	}
	/**
	 * @param index
	 * @return scope object for the specific iterator index.
	 */
	public JavaScope getScope(int index) {
		return getCompilationUnit().getScope(index);
	}
	/**
	 * @return <code>true</code> if the class this descriptor is
	 * reflecting represents an array of elements.
	 */
	public boolean isArray() {
		return arrayElement!=null;
	}
	/**
	 * @return class descriptor for an array element or
	 * <code>null</code> if this descriptor reflects non-array type.
	 */
	public JavaClassDescriptor getArrayElement() {
		return arrayElement;
	}

	private class CompilationUnitScanner 
			extends JavaScanner implements Processor {
		private Stack<List<JavaVariable>> vars = new Stack<List<JavaVariable>>();
		private JavaScope currentScope;
		private JavaTypeScope currentTypeScope;
		
		private CompilationUnitScanner(){}

		@Override
		protected List<Processor> getPreprocessorsForKind(int kind) {
			return Collections.singletonList((Processor)this);
		}

		public void visitStart(TokenChain<? extends Token> iterator, int kind) {
			switch(kind){
			case JavaParserTreeConstants.JJTCOMPILATIONUNIT:
			case JavaParserTreeConstants.JJTBLOCK:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
			case JavaParserTreeConstants.JJTCLASSORINTERFACEDECLARATION:
				openScope(iterator,kind);
				return;
			case JavaParserTreeConstants.JJTCLASSORINTERFACEBODY:
				setTypeName(iterator);
				return;
			case JavaParserTreeConstants.JJTFIELDDECLARATION:
				vars.peek().add(readVar(iterator));
				return;
			case JavaParserTreeConstants.JJTCONSTRUCTORDECLARATION:
				openConstructorScope(iterator);
				return;
			case JavaParserTreeConstants.JJTVARIABLEDECLARATORID:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
				iterator.next();
				vars.peek().add(readVar(iterator));
				return;
			}
		}

		private void openScope(TokenChain<? extends Token> iterator, int kind) {
			JavaScope old = currentScope;
			int index = iterator==null?0:iterator.index();
			if(old==null){
				currentTypeScope = new JavaTypeScope(null, 
						index, addVarsToStack(),loader);
				compilationUnit = currentTypeScope;
				currentScope = currentTypeScope;
				return;
			}
			if(kind==JavaParserTreeConstants
				.JJTCLASSORINTERFACEDECLARATION){
				// reference comparison is not a mistake:
				// instance is the same
				if(currentTypeScope == currentScope){
					return;
				}
				currentTypeScope = new JavaTypeScope(old, 
					index, addVarsToStack(),loader);
				currentScope = currentTypeScope;
			}
			else {
				currentScope = new JavaScope(old,index,addVarsToStack());
			}
			old.addChild(currentScope);
		}

		private List<JavaVariable> addVarsToStack() {
			List<JavaVariable> varsList = new LinkedList<JavaVariable>();
			vars.push(varsList);
			return varsList;
		}

		private void setTypeName(TokenChain<? extends Token> iterator) {
			if(currentTypeScope != currentScope){
				openAnonimousTypeScope(iterator);
				return;
			}
			while(!iterator.previous().getToken().equals(CLASS_KEYWORD));
			String name = iterator.next().getToken();
			currentTypeScope.setClassName(name);
			currentTypeScope.setEntityType(iterator.previous().getToken());
			// clear for top-level anonymous classes
			currentTypeScope = null;
		}

		private void openAnonimousTypeScope(TokenChain<? extends Token> iterator) {
			JavaTypeScope newScope = new JavaTypeScope(currentScope, 
					iterator.index(), addVarsToStack(),loader);
			currentScope.addChild(newScope);
			int deep = 1;
			while(deep>0){
				if(iterator.previous().getToken().equals("(")){
					deep--;
				}
				else if(iterator.current().getToken().equals(")")){
					deep++;
				}
			}
			String type = iterator.previous().getToken();
			JavaTypeScope typeScope = getCompilationUnit()
				.resolveTypeDescriptor(type).getCompilationUnit();
			if(typeScope.isInterface()){
				newScope.addImplementedInterface(type);
			}
			else {
				newScope.setExtendedClass(type);
			}
			currentScope = newScope;
		}

		private JavaVariable readVar(TokenChain<? extends Token> iterator) {
			String name = iterator.current().getToken();
			int index = iterator.index();
			skipTypeParameters(iterator);
			skipPackage(iterator);
			int startOfDeclaration = iterator.index();
			StringBuilder type = new StringBuilder();
			while(iterator.index()<index){
				type.append(iterator.current().getToken());
				iterator.next();
			}
			iterator.moveTo(startOfDeclaration);
			return new JavaVariable(name,
				type.toString(),index,currentScope);
		}

		private void skipPackage(TokenChain<? extends Token> iterator) {
			while(iterator.previous().getToken().equals(".")){
				iterator.previous();
			}
			iterator.next();
		}

		private void skipTypeParameters(TokenChain<? extends Token> iterator) {
			if(!iterator.previous().getToken().equals(">"))return;
			int deep = 1;
			while(deep>0){
				if(iterator.previous().getToken().equals(">"))deep++;
				else if(iterator.current().getToken().equals("<"))deep--;
			}
			iterator.previous();
		}

		public void visitEnd(TokenChain<? extends Token> iterator, int kind) {
			switch(kind){
			case JavaParserTreeConstants.JJTCONSTRUCTORDECLARATION:
				closeConstructorScope(iterator);
			case JavaParserTreeConstants.JJTBLOCK:
				if(currentScope instanceof JavaTypeScope){
					return;
				}
			case JavaParserTreeConstants.JJTCLASSORINTERFACEBODY:
			case JavaParserTreeConstants.JJTMETHODDECLARATION:
				currentScope.endScope(iterator.index());
				currentScope = currentScope.getParent();
				return;
			case JavaParserTreeConstants.JJTIMPORTDECLARATION:
				while(!iterator.previous().getToken().equals(IMPORT_KEYWORD));
				StringBuilder imp = new StringBuilder();
				while(!iterator.next().getToken().equals(END_LINE_KEYWORD)){
					imp.append(iterator.current().getToken());
				}
				((JavaTypeScope)currentScope).addImport(imp.toString());
				return;
			case JavaParserTreeConstants.JJTPACKAGEDECLARATION:
				while(!iterator.previous().getToken().equals(PACKAGE_KEYWORD));
				StringBuilder pkg = new StringBuilder();
				while(!iterator.next().getToken().equals(END_LINE_KEYWORD)){
					pkg.append(iterator.current().getToken());
				}
				((JavaTypeScope)currentScope).setCurrentPackage(pkg.toString());
				return;
			case JavaParserTreeConstants.JJTMETHODDECLARATOR:
				readMethod(iterator);
				return;
			case JavaParserTreeConstants.JJTEXTENDSLIST:
				iterator.next();
				((JavaTypeScope)currentScope).setExtendedClass(
						readType(iterator));
				return;
			case JavaParserTreeConstants.JJTIMPLEMENTSLIST:
				iterator.next();
				do {
					((JavaTypeScope)currentScope).addImplementedInterface(
						readType(iterator));
				}
				while(!iterator.previous().getToken().equals("implements"));
				return;
			}
		}

		private void openConstructorScope(TokenChain<? extends Token> iterator) {
			 JavaTypeScope classScope = (JavaTypeScope)currentScope;
			 currentScope = new JavaConstructor(
					classScope.getClassName(),
					currentScope,iterator.index(),
					addVarsToStack());
			 classScope.addChild(currentScope);
		}

		private void closeConstructorScope(TokenChain<? extends Token> iterator) {
			JavaConstructor c = (JavaConstructor)currentScope;
			int index = iterator.index();
			int deep = 1;
			while(deep>0){
				if(iterator.previous().getToken().equals("{")){
					deep--;
				}
				else if(iterator.current().getToken().equals("}")){
					deep++;
				}
			}
			while(!iterator.previous().getToken().equals(")"));
			List<JavaVariable> parameters = readMethodParameters(iterator);
			List<JavaVariable> vars = this.vars.peek();
			for(JavaVariable var : parameters){
				var.setScope(c);
				vars.add(0, var);
				c.addParameter(var.getType());
			}
			iterator.moveTo(index);
		}

		private void readMethod(TokenChain<? extends Token> iterator) {
			List<JavaVariable> parameters = readMethodParameters(iterator);
			vars.push(parameters);
			String name = iterator.previous().getToken();
			JavaTypeScope classScope = (JavaTypeScope)currentScope;
			currentScope = new JavaMethod(name,currentScope,iterator.index(),
				parameters,readType(iterator));
			classScope.addChild(currentScope);
		}

		private String readType(TokenChain<? extends Token> iterator) {
			int index = iterator.index();
			skipTypeParameters(iterator);
			skipPackage(iterator);
			int next = iterator.index();
			StringBuilder type = new StringBuilder();
			while(iterator.index()<index){
				type.append(iterator.current().getToken());
				iterator.next();
			}
			iterator.moveTo(next);
			return type.toString();
		}

		private List<JavaVariable> readMethodParameters(
				TokenChain<? extends Token> iterator) {
			List<JavaVariable> result = new LinkedList<JavaVariable>();
			while(!iterator.previous().getToken().equals("(")){
				if(!result.isEmpty())iterator.previous();
				result.add(0, readParameter(iterator));
			}
			return result;
		}
		
		private JavaVariable readParameter(
				TokenChain<? extends Token> iterator){
			String name = iterator.current().getToken();
			boolean varArg = isVarArg(iterator);
			int index = iterator.index();
			skipTypeParameters(iterator);
			skipPackage(iterator);
			StringBuilder type = new StringBuilder();
			while(iterator.index()<index){
				type.append(iterator.current().getToken());
				iterator.next();
			}
			while(!iterator.previous().getToken().equals(",")&&
				!iterator.current().getToken().equals("("));
			iterator.next();
			return new JavaVariable(name,type.toString(),
					index,currentScope,varArg);
		}

		private boolean isVarArg(TokenChain<? extends Token> iterator) {
			if(!iterator.previous().getToken().equals("...")){
				iterator.next();
				return false;
			}
			return true;
		}
	}
}
